--[[
英雄无敌3地图编辑器
wangdm001@163.com, 2025-8

TODO : allowedHeroes数组不全，heroConfigurationCustomizations 中提到的英雄应该加入到该数组里
BUG  : heroConfigurationCustomizations数组中的名字和官方地图编辑器对不上

Usage :

lua h3mdump [debug=1] [write=1] [lua=<lua dump file>] [xml=<xml dump file>] <map.h3m>

]]
local rtk = require("libluartk")
local gx = require("libgamex")
local json = require("cjson")
require("games.heroes3.h3ids")
local bit32 = gx.bit32
local lpeg = require("lpeg")
local globals = {}

if rtk.getpropFlag("debug.map") then
	map_debug = function(...)
		rtk.printk(1, ...)
	end
else
	map_debug = function() end
end
local C, P, Cmt, Ct, Cg, Cb, Cp, Cc, Carg =
	lpeg.C, lpeg.P, lpeg.Cmt, lpeg.Ct, lpeg.Cg, lpeg.Cb, lpeg.Cp, lpeg.Cc, lpeg.Carg
local Offset = Cp() / function(pos)
	return string.format("0x%x", pos - 1)
end
local function CMarker(name)
	return P(function(sub, pos)
		map_debug('-- "%s"=0x%x --\n', name or "marker", pos - 1)
		return true
	end)
end
local False = P("\0")
local True = 1 - False
local function Inc(v)
	return v + 1
end
local function Byte(c)
	return P(string.char(c))
end
local function CByte(c)
	return P(string.char(c)) * Cc(c)
end
local CField = function(name, patt)
	return Cg(patt, name)
end
-- make a backreferece globally visible
local CGlobal = function(ref)
	return Cmt(Cb(ref), function(sub, pos, val)
		--		map_debug('Setting globals.%s=%s\n',ref,tableToLuaCode(val))
		globals[ref] = val
		return true
	end)
end
local function Enum(patt, lkup_tbl)
	return patt
		/ function(v)
			local nv = lkup_tbl[v]
			--		assert(nv,string.format('Invalid enum value:%d',v))
			return nv or v
		end
end
-- a one byte bool value, and if true, followed by details
local function BoolCond(patt, else_part)
	return True * patt + False * (else_part or true)
end

local function BoolField(name, patt)
	return BoolCond(CField(name, patt))
end

--[[
	A general optional field.
]]
local function If(pred_pattern, pattern, else_part)
	return P(pred_pattern) * pattern + (else_part or P(true))
end

--
-- 检查
-- ref_name可以是字符串，代表backref，或者任意模式
-- ref_val可以是函数或者constant，如果是constant，则ref_name或者其代表的模式的
-- capture value用用来和该constant做相等比较；如果是函数，该函数负责判断ref_name
-- 是否满足要求
local function CCheck(ref_name, ref_val)
	if type(ref_name) == "string" then
		ref_name = Cb(ref_name)
	else
		ref_name = C(ref_name)
	end
	if ref_val == nil or ref_val == true then
		ref_val = function(ref)
			return ref
		end
	elseif type(ref_val) ~= "function" then
		ref_val = function(ref)
			return ref == ref_val
		end
	end
	return Cmt(ref_name, function(sub, pos, val)
		local cond = ref_val(val)
		--map_debug('CCheck = %s\n',tostring(cond))
		return cond
	end)
end

local function CEqualConst(patt_1, const)
	return Cmt(Ct(patt_1), function(sub, pos, v1, v2)
		assert(#v1 == 1, string.format("Invalid CEqual call : pattern should have exactly 1 capture."))
		return v1[1] == const
	end)
end

local function CEqual(patt_1, patt_2)
	Cmt(Ct(patt_1) * Ct(patt_2), function(sub, pos, v1, v2)
		assert(
			#v1 == 1 and #v2 == 1,
			string.format("Invalid CEqual call : either pattern should have exactly 1 capture.")
		)
		return v1[1] == v2[2]
	end)
end

local uint4 = C(4) / function(s)
	return gx.read_uint32(s, 0)
end
local uint2 = C(2) / function(s)
	return gx.read_uint16(s, 0)
end
local uint1 = C(1) / function(s)
	return gx.read_uint8(s, 0)
end
local int4 = C(4) / function(s)
	return gx.read_int32(s, 0)
end
local int2 = C(2) / function(s)
	return gx.read_int16(s, 0)
end
local int1 = C(1) / function(s)
	return gx.read_int8(s, 0)
end
local byte = C(1) / string.byte
local bool = C(1) / function(b)
	return b ~= "\0"
end

local pstring = Cmt(uint4, function(sub, pos, len)
	local str
	if pos + len - 1 > #sub then
		rtk.printk(2, "string at 0x%x of len 0x%x exceeds limit(0x%x).\n", pos - 1, len, #sub)
		return
	end
	if len > 0 then
		str = sub:sub(pos, pos + len - 1)
	else
		str = ""
	end
	map_debug('pstring(0x%x,0x%x)="%s"\n', pos - 1, len, str)
	local strobj = { pos = pos - 1, len = len, str = str }
	table.insert(globals.strings, strobj)
	return pos + len, strobj
end)

local CFileName = Cmt(uint4, function(sub, pos, len)
	local str
	if pos + len - 1 > #sub then
		rtk.printk(2, "Filename at 0x%x of len 0x%x exceeds limit(0x%x).\n", pos - 1, len, #sub)
		return
	end
	if len > 0 then
		str = sub:sub(pos, pos + len - 1)
	else
		str = ""
	end
	map_debug('Filename(0x%x,0x%x)="%s"\n', pos - 1, len, str)
	local strobj = { pos = pos - 1, len = len, file = str }
	return pos + len, strobj
end)

local function hex(n)
	return C(n)
		/ function(s)
			local ret = {}
			local i
			for i = 1, #s do
				table.insert(ret, string.format("%02x", s:byte(i)))
			end
			return table.concat(ret)
		end
end

local CSized = function(patt)
	return Cp()
		* lpeg.Ct(patt)
		* Cp()
		/ function(bpos, t, epos)
			t.FBeginPos = string.format("0x%x", bpos - 1)
			t.FEndPos = string.format("0x%x", epos - 1)
			t.FTagSize = string.format("0x%x", epos - bpos)
			return t
		end
end

local function readArrayEntry(patt, sub, pos, hint, cnt, idx)
	local ret, npos
	ret = { (CField("arrIdx", Cc(idx - 1)) * patt * Cp()):match(sub, pos) }
	--	map_debug('%s[%d] #ret=%d\n',hint,idx,#ret)
	assert(#ret == 2, string.format("Entries in array should have only 1 capture."))
	npos = ret[2]
	ret = ret[1]
	if debug_flag and hint then
		local txt
		--txt=json.encode(ret)
		if type(ret) == "table" then
			txt = rtk.tableToLuaCode(ret)
		else
			txt = json.encode(ret)
		end
		map_debug("%s[%d/%d] @ 0x%x -> 0x%x=%s\n", hint, idx, cnt, pos - 1, npos - 1, txt)
	end
	return npos, ret
end

local function CByteArray(count)
	return Ct(C(count) / function(s)
		return string.byte(s, 1, #s)
	end)
end

local CArray = function(count, patt, hint)
	return Cmt(true, function(sub, pos)
		local i
		local arr = {}
		for i = 1, count do
			local ent
			pos, ent = readArrayEntry(patt, sub, pos, hint, count, i)
			arr[i] = ent
		end
		return pos, arr
	end)
end

local function CDynamicArray(cnt_pattern, patt, hint)
	return Cmt(Cp() * cnt_pattern, function(sub, pos, arr_pos, cnt)
		if debug_flag and hint then
			map_debug("Reading %s of %d entries at 0x%x:0x%x\n", hint, cnt, arr_pos - 1, pos - 1)
		end
		local i
		local arr = {}
		local entry
		for i = 1, cnt do
			pos, entry = readArrayEntry(patt, sub, pos, hint, cnt, i)
			arr[i] = entry
		end
		return pos, arr
	end)
end

--[[
function test()
	ele=Ct(CField('idx',Cb 'arrIdx')*CField('val',int4))
	patt=CDynamicArray(int1,ele,'test')
	str='\x03\x01\0\0\0\x03\0\0\0\x05\0\0\0'
	tprint(patt:match(str))
	os.exit()
end
test()
]]

local function convertBytesToBitArray(arr, cnt)
	local i
	local byte_cnt = bit32.rshift(cnt + 7, 3)
	local bit_arr = {}
	for i = 1, byte_cnt do
		local j
		local bit = arr[i]
		for j = 1, 8 do
			if #bit_arr >= cnt then
				break
			end
			table.insert(bit_arr, bit32.band(bit, 1) == 1)
			bit = bit32.rshift(bit, 1)
		end
	end
	return bit_arr
end

local function CBitArray(n)
	local byte_cnt = bit32.rshift(n + 7, 3)
	return C(byte_cnt) / function(arr)
		arr = { arr:byte(1, #arr) }
		return convertBytesToBitArray(arr, n)
	end
end

local function CDynamicBitArray(cnt_pattern)
	return Cmt(cnt_pattern, function(sub, pos, cnt)
		local byte_cnt = bit32.rshift(cnt + 7, 3)
		local arr = { sub:byte(pos, pos + byte_cnt - 1) }
		local bit_arr = convertBytesToBitArray(arr, cnt)
		return pos + byte_cnt, bit_arr
	end)
end

local Sex = Byte(0) / "男" + Byte(1) / "女" + Byte(0xff) / "默认" + int1

local g_map_info

local function MapFormatIs(fmt)
	return P(function()
		--		map_debug('Checking Format "%s" == "%s"\n',g_map_info.format,fmt)
		return g_map_info.magic == H3MapInfoMagics[fmt]
	end)
end
local function MapFormatGE(fmt)
	return P(function()
		--		map_debug('Checking Format "%s" >= "%s"\n',g_map_info.format,fmt)
		return g_map_info.magic >= H3MapInfoMagics[fmt]
	end)
end
local MonsterId = Enum(MapFormatIs("RoE") * int1 + int2, H3Creatures)
local ArtifactId = Enum(MapFormatIs("RoE") * int1 + int2, H3Artifacts)

function getAllowedFactionNames(mask)
	local names = {}
	local i
	for i = 0, 7 do
		if bit32.band(mask, 1) ~= 0 then
			table.insert(names, H3FactionNames[i])
		end
		mask = bit32.rshift(mask, 1)
	end
	return names
end
function getFactionName(id)
	return H3FactionNames[id] or id
end

local MapInfo = Cmt(
	Ct(CField("magic", uint4) * If(
		Cmt(Cb("magic"), function(s, p, m)
			return m == H3MapInfoMagics.HotA
		end),
		CField("subFormat", uint1)
			* If(
				Cmt(Cb("subFormat"), function(s, p, v)
					return v == 1
				end),
				P(4) * CField("arena", bool),
				P(3)
			),
		-- 这是我加的，非HotA地图其实没有subFormat字段
		CField("subFormat", Cc(-1))
	) * CField("hasHeroes", bool) * CField("size", uint4) * CField("hasSecondLevel", bool) * CField("name", pstring) * CField(
		"description",
		pstring
	) * CField("difficulty", uint1) * If(
		CCheck("magic", function(v)
			return v >= H3MapInfoMagics.AB
		end),
		CField("experienceLevelLimit", uint1)
	)),
	function(s, p, info)
		info.format = H3MapInfoMagics[info.magic]
		g_map_info = info
		return true, info
	end
)

local Position = Ct(CField("x", uint1) * CField("y", uint1) * CField("z", uint1))

local PlayerHero = Ct(CField("id", uint1 / H3Heroes) * CField("name", pstring))

local Player = Ct(
	P(function(sub, pos)
		map_debug("Reading player at 0x%x\n", pos - 1)
		return true
	end)
		* CField("canHumanPlay", bool)
		* CField("canAIPlay", bool)
		* CField("AITactic", uint1)
		* If(MapFormatGE("SoD"), CField("allowedFactionsCustomised", bool))
		* CField("allowedFactions", If(MapFormatIs("RoE"), uint1, uint2) / getAllowedFactionNames)
		* CField("isFactionRandom", bool)
		* BoolField(
			"mainTown",
			Ct(
				If(MapFormatGE("AB"), CField("generateHero", bool) * CField("faction", Enum(uint1, H3FactionNames)))
					* CMarker("pos")
					* CField("pos", Position)
			)
		)
		* CField("hasRandomHeroes", bool)
		* If(
			Byte(0xff),
			true,
			CField(
				"mainHero",
				Ct(CField("id", Enum(uint1, H3Heroes)) * CField("portraitId", uint1) * CField("name", pstring))
			)
		)
		* If(
			MapFormatGE("AB"),
			CField("heroPlaceholderCount", uint1) * CField("heroes", CDynamicArray(uint4, PlayerHero, "PlayerHero"))
		)
)

local VictoryCond = {}
rtk.table_each(H3VictoryConditionTypes, function(k, v)
	if type(v) == "number" then
		VictoryCond[k] = CField("type", Byte(v) * Cc(k))
			* CField("allowNormalVictory", bool)
			* CField("appliesToAI", bool)
	end
end)
VictoryCond.WinStandard = CField("type", Byte(0xff) / "WinStandard")

local VictArtifact = VictoryCond.Artifact * CField("id", uint1) * If(MapFormatGE("AB"), P(1))
local GatherTroop = VictoryCond.GatherTroop * CField("id", MonsterId) * CField("value", uint4)
local GatherResource = VictoryCond.GatherResource * CField("id", Enum(uint1, H3ResourceIds)) * CField("value", uint4)
local BuildCity = VictoryCond.BuildCity
	* CField("position", Position)
	* CField("hallLevel", uint1)
	* CField("castleLevel", uint1)
local BuildGrail = VictoryCond.BuildGrail * CField("position", Position)
local BeatHero = VictoryCond.BeatHero * CField("position", Position)
local CaptureCity = VictoryCond.CaptureCity * CField("position", Position)
local BeatMonster = VictoryCond.BeatMonster * CField("position", Position)
local TransportItem = VictoryCond.TransportItem * CField("id", uint1 / H3Artifacts) * CField("position", Position)
local TakeDwellings = VictoryCond.TakeDwellings
local TakeMines = VictoryCond.TakeMines
local BeatAllMonsters = VictoryCond.BeatAllMonsters
local Survive = VictoryCond.Survive * CField("value", uint4)
local WinStandard = VictoryCond.WinStandard

local VictoryCondition = Ct(
	VictArtifact
		+ GatherTroop
		+ GatherResource
		+ BuildCity
		+ BuildGrail
		+ BeatHero
		+ CaptureCity
		+ BeatMonster
		+ TransportItem
		+ TakeDwellings
		+ TakeMines
		+ BeatAllMonsters
		+ Survive
		+ WinStandard
		+ C(1) / function(tp)
			error("Invalid victory type:" .. tp:byte(1))
		end
)

local LossTown = CField("type", C("\0") / "LossTown") * CField("position", Position)
local LossHero = CField("type", C("\1") / "LossHero") * CField("position", Position)
local TimeExpires = CField("type", C("\2") / "TimeExpires") * CField("value", uint2)
local LossStandard = CField("type", C("\xff") / "LossStandard")

local LossCondition = Ct(LossTown + LossHero + TimeExpires + LossStandard + C(1) * Cp() / function(tp, pos)
	error(string.format("Invalid lose type:%d at 0x%x", tp:byte(1), pos))
end)

local NoTeam = CField("teamCount", P("\0") * Cc(0))
local TeamDef = CField("teamCount", uint1) * CField("teamIds", CByteArray(8))

local function getAllowdHeroMask(arr)
	local ret = { masks = arr }
	local bc = #arr
	local i
	local n = 0
	local disabled = {}
	for i = 1, bc do
		bits = arr[i]
		local j
		for j = 1, 8 do
			if bit32.band(bits, 1) ~= 1 then
				table.insert(disabled, n)
			end
			bits = bit32.rshift(bits, 1)
			n = n + 1
		end
	end
	ret.disabled = disabled
	return ret
end

local AllowedHeroArray = Ct(
	CField("count", MapFormatIs("HotA") * uint4 + MapFormatIs("RoE") * Cc(128) + Cc(160))
		* CField(
			"heroes",
			Cmt(Cb("count"), function(subject, pos, cnt)
				local bcount = bit32.rshift(cnt + 7, 3)
				local arr = { subject:byte(pos, pos + bcount - 1) }
				return pos + bcount, getAllowdHeroMask(arr)
			end)
		)
)

local HeroCustomization =
	Ct(CField("id", uint1) * CField("portraitId", uint1) * CField("name", pstring) * CField("AllowedForPlayers", uint1))

local HeroCustomizationArray = Ct(CField("count", uint1) * CField(
	"array",
	Cmt(Cb("count"), function(sub, pos, cnt)
		local i
		local array = {}
		for i = 1, cnt do
			local h
			h, pos = (HeroCustomization * Cp()):match(sub, pos)
			table.insert(array, h)
		end
		return pos, array
	end)
))

local AllowedArtifactsCount = Cmt(MapFormatIs("HotA"), function(sub, pos)
	local n = sub:byte(pos)
	if n == 16 then
		pos = pos + 5
		n = sub:byte(pos)
	end
	pos = pos + 3
	return pos, n
end) + MapFormatIs("AB") * Cc(129) + Cc(144)

local function InterpreteInvertBitArray(names)
	return function(arr)
		local ret = { mask = arr }
		local i
		local disabled = {}
		for i = 1, #arr do
			if arr[i] then
				table.insert(disabled, { id = i - 1, name = names[i - 1] })
			end
		end
		ret.disabled = disabled
		return ret
	end
end

-- 选择一个bool数组的true部分或者false部分，返回下标数组，names可选可将结果翻译成字符串
-- names下标从0开始,bool数组arr从1开始
local function selectBitArray(names, filter_val)
	names = names or {}
	return function(arr)
		local ret = {}
		local k, v
		if filter_val == nil then
			-- not specified, default to true
			filter_val = true
		elseif filter_val ~= true then
			filter_val = false
		end
		for k, v in ipairs(arr) do
			if v == filter_val then
				table.insert(ret, names[k - 1] or k - 1) -- {id=k-1,name=names[k-1]})
			end
		end
		return ret
	end
end

local AllowedArtifacts = CDynamicBitArray(AllowedArtifactsCount) / InterpreteInvertBitArray(H3Artifacts)
local AllowedSpells = CBitArray(70) / InterpreteInvertBitArray(H3Spells)
local AllowedSecodarySkills = CBitArray(28) / InterpreteInvertBitArray(H3SecondarySkills)

local Rumors = CDynamicArray(uint4, Ct(CField("name", pstring) * CField("value", pstring)), "rumors")

local HeroInventory = Ct(
	CArray(18, ArtifactId)
		* If(MapFormatGE("SoD"), CField("misc5", ArtifactId))
		* CField("bag", CDynamicArray(uint2, ArtifactId, "artifacts"))
)

local HeroSpells = CBitArray(70)
	/ function(bits)
		local set = {}
		local k, v
		for k, v in ipairs(bits) do
			if v then
				table.insert(set, H3Spells[k - 1])
			end
		end
		return set
	end

local MapObjectHero_SecondarySkills = CDynamicArray(uint4, Ct(uint1 / H3SecondarySkills * uint1), "secondSkills")

local HeroConfigurationCustomization = True
		* CSized(
			BoolField("experience", uint4)
				* CField("idx", Cb("arrIdx"))
				* CField("name", Cb("arrIdx") / H3Heroes)
				* BoolField("secondarySkills", MapObjectHero_SecondarySkills)
				* BoolField("artifacts", HeroInventory)
				* BoolField("biography", pstring)
				* CField("sex", Sex)
				* BoolField("spells", HeroSpells)
				* BoolField("primarySkills", CArray(4, uint1))
		)
	+ False * Cc({})

local HeroConfigurationCustomizations =
	CDynamicArray(MapFormatIs("HotA") * uint4 + Cc(156), HeroConfigurationCustomization, "HeroConfig")

local MapSize = P(function()
	return true, g_map_info.size
end)
--[[
local TerrainTile=Ct(
	CField('type',uint1/H3TerrainTypes)
	* CField('variant',uint1)
	* CField('riverType',uint1)
	* CField('riverDirection',uint1)
	* CField('roadType',uint1)
	* CField('roadDirection',uint1)
	* CField('displayOptions',uint1)
)
]]
local TerrainTile = hex(7)
local TerrainRow = CDynamicArray(MapSize, TerrainTile)
local Terrain = CDynamicArray(MapSize, TerrainRow)
local BitMask = function(byte_count)
	return hex(byte_count)
end

local Resources = CArray(7, int4)
local MapObjectTemplate = Ct(
	CField("animFile", CFileName)
		* CField("blockMask", BitMask(6))
		* CField("visitMask", BitMask(6))
		* CField("supportedTerrainTypes", uint2)
		* CField("editorMenuLocation", uint2)
		* CField("id", uint4)
		* CField("subId", uint4)
		* CField("type", uint1 / H3ObjectTypes)
		* CField("isBackground", bool)
		* P(16)
)
local MapObjectTemplates = CDynamicArray(uint4, MapObjectTemplate, "mapObjTemplates")

local function Creatures(n)
	return CArray(n, Ct(CField("type", MonsterId) * CField("count", int2)))
end
local MapObject_SubId = P(function()
	return true, globals.mapObjectTemplate.subId
end)
local MapObjectIs = function(name)
	return Cmt(Cb("oid"), function(sub, pos, tid)
		return tid == H3ObjectIds[name]
	end)
end

local MapObjectReader = {}
-- initialize reader, accept all assuming that no special treatment is needed
rtk.table_each(H3ObjectIds, function(k, v)
	if type(v) == "number" then
		--		map_debug('Init type %d(%s)\n',v,k)
		MapObjectReader[v] = Cc({})
	end
end)
local function MapObjectUnhandled(tp)
	return P(function(sub, pos)
		error(string.format("Cannot read object at 0x%x of type %d.", pos - 1, tp))
	end)
end
local function handle(tp, patt)
	tp = H3ObjectIds[tp]
	MapObjectReader[tp] = patt or MapObjectUnhandled(tp)
end

local MapObjectEvent_Base = BoolCond(CField("msg", pstring) * BoolField("guard", Creatures(7)) * P(4))
	* CField("exp", uint4)
	* CField("mana", uint4)
	* CField("morale", int1)
	* CField("luck", int1)
	* CField("resources", Resources)
	* CField("primarySkills", CArray(4, uint1))
	* CField("secondarySkills", CDynamicArray(uint1, Ct(uint1 / H3SecondarySkills * uint1), "secondSkills"))
	* CField("artifacts", CDynamicArray(uint1, ArtifactId, "artifacts"))
	* CField("spells", CDynamicArray(uint1, uint1 / H3Spells), "spells")
	* CField("monsters", CDynamicArray(uint1, Ct(MonsterId * uint2)), "monsters")
	* P(8)

local MapObjectEvent = Ct(
	MapObjectEvent_Base
		* CField("triggeredByPlayer", bool)
		* CField("triggeredByAI", bool)
		* CField("cancelAfterFirstVisit", bool)
		* P(4)
)

local MapObjectHero = Ct(
	If(MapFormatGE("AB"), CField("identifier", hex(4)))
		* CField("owner", uint1)
		* CField("id", uint1 / function(id)
			local h = id ~= 0xff and { id = id, name = H3Heroes[id] } or { id = id }
			return h
		end)
		* (BoolField("name", pstring))
		* (MapFormatGE("SoD") * (BoolField("experience", uint4)) + CField("experience", uint4))
		* (BoolField("portrait", uint1))
		* (BoolField("secondSkills", MapObjectHero_SecondarySkills))
		* (BoolField("army", Creatures(7)))
		* CField("armyFormation", uint1)
		* (BoolField("artifacts", HeroInventory))
		* CField("patrolRadius", uint1)
		* If(MapFormatGE("AB"), BoolField("biography", pstring) * CField("sex", Sex))
		* (MapFormatIs("AB") * CField("spellId", uint1) + MapFormatGE("SoD") * BoolField("spells", HeroSpells) * BoolField(
			"primarySkills",
			CArray(4, uint1)
		) + true)
		* P(16)
)
local MapObjectMonster = Ct(
	If(MapFormatGE("AB"), CField("identifier", hex(4)))
		* CField("count", uint2)
		* CField("disp", uint1)
		* (True * CField("message", pstring) * CField("resources", Resources) * CField("artifact", ArtifactId) + False)
		* CField("alwaysAttacks", bool)
		* CField("keepSize", bool)
		* P(2)
)

local MapObjectMessage = Ct(CField("msg", pstring) * P(4))
local QuestTypeIs = function(n)
	return Cmt(Cb("qtype"), function(s, p, tp)
		return tp == n
	end)
end

local MapObjectSeersHut_Reward = Ct(
	CByte(H3RewardTypes.None)
		+ CByte(H3RewardTypes.Experience) * CField("value", uint4)
		+ CByte(H3RewardTypes.SpellPoints) * CField("value", int4)
		+ (CByte(H3RewardTypes.Morale) + CByte(H3RewardTypes.Luck)) * CField("value", int1)
		+ CByte(H3RewardTypes.Resource) * CField("resource", Enum(uint1, H3ResourceIds)) * CField("value", int4)
		+ CByte(H3RewardTypes.PrimarySkill) * CField("skill", Enum(uint1, H3PrimarySkills)) * CField("value", uint1)
		+ CByte(H3RewardTypes.SecondarySkill) * CField("skill", Enum(uint1, H3SecondarySkills)) * CField("level", uint1)
		+ CByte(H3RewardTypes.Artifact) * CField("artifact", ArtifactId)
		+ CByte(H3RewardTypes.Spell) * CField("spell", Enum(uint1, H3Spells))
		+ CByte(H3RewardTypes.Creatures) * CField("monster", MonsterId) * CField("value", uint2)
		+ Cmt(uint1, function(s, p, tp)
			error(string.format("Invalid reward type %d at 0x%x", tp, p - 1))
		end)
) / function(a)
	local tp = a[1]
	a.type = H3RewardTypes[tp]
	table.remove(a, 1)
	--	map_debug('obj=%s\n',tableToLuaCode(a))
	return a
end

local MapObject_Quest_RoE = Ct(
	CField("qtype", Cc(H3QuestTypes.ReturnWithArtifacts))
		* CField("qname", Cc("ReturnWithArtifacts"))
		* CField("artifacts", ArtifactId / function(a)
			return { a }
		end)
)

local MapObject_Quest_AB = Ct(
	CField("qtype", uint1)
		* CField("qname", Cb("qtype") / H3QuestTypes)
		* (QuestTypeIs(H3QuestTypes.AchievePrimarySkillLevel) * CField("primarySkills", Ct(CArray(4, uint1))) + QuestTypeIs(
			H3QuestTypes.AchieveExperienceLevel
		) * CField("expLevel", uint4) + (QuestTypeIs(H3QuestTypes.DefeatASpecificHero) + QuestTypeIs(
			H3QuestTypes.DefeatASpecificMonster
		)) * CField("refId", hex(4)) + QuestTypeIs(H3QuestTypes.ReturnWithArtifacts) * CField(
			"artifacts",
			CDynamicArray(uint1, ArtifactId, "artifacts")
		) + QuestTypeIs(H3QuestTypes.ReturnWithCreatures) * CField(
			"creatures",
			CDynamicArray(uint1, Ct(MonsterId * uint2), "creatures")
		) + QuestTypeIs(H3QuestTypes.ReturnWithResources) * CField("resources", Resources) + QuestTypeIs(
			H3QuestTypes.BeASpecificHero
		) * CField("heroId", uint1 / H3Heroes) + QuestTypeIs(H3QuestTypes.BelongToASpecificPlayer) * CField(
			"player",
			uint1 / H3PlayerColors
		) + QuestTypeIs(H3QuestTypes.None) + Cmt(Cb("qtype"), function(sub, pos, tp)
			error(string.format("Invalid quest type:%d", tp))
		end))
		* If(
			QuestTypeIs(H3QuestTypes.None),
			true,
			CField("deadline", int4)
				* CField("firstVisitText", pstring)
				* CField("nextVisitText", pstring)
				* CField("completeText", pstring)
		)
)
local MapObject_Quest = If(MapFormatIs("RoE"), MapObject_Quest_RoE, MapObject_Quest_AB)

local MapObjectSeersHut = Ct(CField("quest", MapObject_Quest) * If(
	Cmt(Cb("quest"), function(s, b, q)
		return q.qtype == H3QuestTypes.None
	end),
	P(3),
	CField("reward", MapObjectSeersHut_Reward) * P(2)
))

local MapObjectWitchHut = Ct(If(
	MapFormatGE("AB"),
	CField("skills", CBitArray(28) / function(arr)
		local ret = {}
		local k, v
		for k, v in ipairs(arr) do
			if v then
				table.insert(ret, H3SecondarySkills[k - 1])
			end
		end
		return ret
	end)
))

local MapObjectScholar = Ct(
	(
		CByte(H3ScholarBonusTypes.Random) * P(1)
		+ CByte(H3ScholarBonusTypes.PrimarySkill) * CField("skill", Enum(uint1, H3PrimarySkills))
		+ CByte(H3ScholarBonusTypes.SecondarySkill) * CField("skill", Enum(uint1, H3SecondarySkills))
		+ CByte(H3ScholarBonusTypes.Spell) * CField("spell", Enum(uint1, H3Spells))
		+ Cmt(uint1, function(s, pos, tp)
			error(string.format("Invalid scholar bonus type %d at 0x%x", tp, pos - 1))
		end)
	) * P(6)
) / function(a)
	local tp = a[1]
	a.type = H3ScholarBonusTypes[tp]
	table.remove(a, 1)
	return a
end

local MapObjectGarrison = function(dir)
	return Ct(
		CField("dir", Cc(dir))
			* CField("owner", uint1 / H3PlayerColors)
			* P(3)
			* CField("creatures", Creatures(7))
			* CField("unitsRemovable", MapFormatIs("RoE") * Cc(true) + bool)
			* P(8)
	)
end

local MapObjectArtifact = Ct(
	BoolCond(CField("msg", pstring) * BoolField("guard", Creatures(7)) * P(4))
		* CField("artifact", MapObject_SubId / H3Artifacts)
)
local MapObjectRandomArtifact = Ct(BoolCond(CField("msg", pstring) * BoolField("guard", Creatures(7)) * P(4)))

local MapObjectSpellScroll =
	Ct(BoolCond(CField("msg", pstring) * BoolField("guard", Creatures(7)) * P(4)) * CField("spell", int4 / H3Spells))

local MapObjectResource = Ct(
	BoolCond(CField("msg", pstring) * BoolField("guard", Creatures(7)) * P(4))
		* CField("amount", int4)
		* P(4)
		* CField("resource", MapObject_SubId / H3ResourceIds)
)

local TownEvent_Base = CField("name", pstring)
	* CField("msg", pstring)
	* CField("resource", Resources)
	* CField("players", hex(1))
	* CField("humanAffected", MapFormatGE("SoD") * bool + Cc(true))
	* CField("aiAffected", bool)
	* CField("FirstOccurence", uint2)
	* CField("RepeatEvery", uint1)
	* P(17)

local TownEvents = CDynamicArray(
	uint4,
	Ct(TownEvent_Base * CField("newBuildings", CBitArray(6 * 8)) * CField("creatures", CArray(7, uint2)) * P(4)),
	"TownEvents"
)

local MapObjectTown = Ct(
	CField("townType", MapObject_SubId)
		* CField("townTypeName", Cb("townType") / H3FactionNames)
		* If(MapFormatGE("AB"), CField("ident", hex(4)))
		* CField("player", uint1 / H3PlayerColors)
		* BoolField("name", pstring)
		* BoolField("garrison", Creatures(7))
		* CField("garrisonFormation", int1)
		* BoolCond(
			CField(
				"customBuilding",
				Ct(
					CField("built", CBitArray(6 * 8) / selectBitArray()) --/selectBitArray(H3Buildings))
						* CField("forbidden", CBitArray(6 * 8) / selectBitArray()) --/selectBitArray(H3Buildings))
				)
			),
			CField("hasFort", bool)
		)
		* If(MapFormatGE("AB"), CField("spellsMustAppear", CBitArray(70) / selectBitArray(H3Spells, true)))
		* CField("spellsMayAppear", CBitArray(70) / selectBitArray(H3Spells, true))
		* If(
			P(function()
				return g_map_info.format == "HotA" and g_map_info.subFormat == 1
			end),
			CField("allowSpellResearch", bool)
		)
		* CField("events", TownEvents)
		* If(MapFormatGE("SoD"), CField("align", uint1))
		* P(3)
)

local MapObjectCreatureGenerator1 = Ct(
	CField("ctype", MapObject_SubId)
		* CField("cname", Cb("ctype") / H3CreatureGenerators_1)
		* CField("owner", uint1 / H3PlayerColors)
		* P(3)
)

local MapObjectCreatureGenerator4 = Ct(
	CField("ctype", MapObject_SubId)
		* CField("cname", Cb("ctype") / H3CreatureGenerators_4)
		* CField("owner", uint1 / H3PlayerColors)
		* P(3)
)

local MapObjectOwner = Ct(CField("owner", uint1 / H3PlayerColors) * P(3))

handle("Event", MapObjectEvent)
handle("Hero", MapObjectHero)
handle("Prison", MapObjectHero)
handle("RandomHero", MapObjectHero)

handle("Monster", MapObjectMonster)
handle("RandomMonster", MapObjectMonster)
handle("RandomMonster1", MapObjectMonster)
handle("RandomMonster2", MapObjectMonster)
handle("RandomMonster3", MapObjectMonster)
handle("RandomMonster4", MapObjectMonster)
handle("RandomMonster5", MapObjectMonster)
handle("RandomMonster6", MapObjectMonster)
handle("RandomMonster7", MapObjectMonster)

handle("OceanBottle", MapObjectMessage)
handle("Sign", MapObjectMessage)

handle("SeersHut", MapObjectSeersHut)
handle("WitchHut", MapObjectWitchHut)

handle("Scholar", MapObjectScholar)

handle("Garrison", MapObjectGarrison("EW"))
handle("Garrison2", MapFormatGE("AB") * MapObjectGarrison("NS"))

handle("Artifact", MapObjectArtifact)
handle("RandomArtifact", MapObjectRandomArtifact)
handle("RandomTreasureArtifact", MapObjectRandomArtifact)
handle("RandomMinorArtifact", MapObjectRandomArtifact)
handle("RandomMajorArtifact", MapObjectRandomArtifact)
handle("RandomRelicArtifact", MapObjectRandomArtifact)

handle("SpellScroll", MapObjectSpellScroll)

handle("Resource", MapObjectResource)
handle("RandomResource", MapObjectResource)

handle("RandomTown", MapObjectTown)
handle("Town", MapObjectTown)

handle("CreatureGenerator1", MapObjectCreatureGenerator1)
handle("CreatureGenerator4", MapObjectCreatureGenerator4)
handle("CreatureGenerator2")
handle("CreatureGenerator3")

handle("Shipyard", MapObjectOwner)
handle("Lighthouse", MapObjectOwner)

--  https://github.com/HeroesProjects/h3mapper
--  的代码在这里似乎有错误，potentialResource实际上用了4个字节
--  而不是1个字节
local MapObjectMine = Ct(
	CField("mineType", MapObject_SubId / H3MineTypes)
		* If(
			CEqualConst(MapObject_SubId, H3MineTypes.AbandonedMine),
			CField("potentialResource", CBitArray(7)) * P(3),
			CField("owner", uint1 / H3PlayerColors) * P(3)
		)
)
handle("Mine", MapObjectMine)
handle("Mine2", MapFormatGE("AB") * MapObjectMine)

local MapObjectShrine = Ct(CField("spell", Enum(uint1, H3Spells)) * P(3) * CField("sub_id", MapObject_SubId))

handle("ShrineOfMagicGesture", MapObjectShrine)
handle("ShrineOfMagicIncantation", MapObjectShrine)
handle("ShrineOfMagicThought", MapObjectShrine)

handle("PandorasBox", Ct(MapObjectEvent_Base))
handle("Grail", Ct(CField("radius", int4)))

local MapObjectRandomDwelling = MapFormatGE("AB")
	* Ct(
		CField("player", uint1 / H3PlayerColors)
			* P(3)
			* If(
				P("\0\0\0\0"),
				CField("allowdFactions", CBitArray(9) / selectBitArray(H3FactionNames)),
				CField("factionSameAsTownId", int4)
			)
			* CField("minLevel", uint1 / Inc)
			* CField("maxLevel", uint1 / Inc)
	)

local MapObjectRandomDwellingFaction = MapFormatGE("AB")
	* Ct(
		CField("player", uint1 / H3PlayerColors)
			* P(3)
			* CField("faction", MapObject_SubId / H3FactionNames)
			* CField("minLevel", uint1 / Inc)
			* CField("maxLevel", uint1 / Inc)
	)

local MapObjectRandomDwellingLevel = MapFormatGE("AB")
	* Ct(
		CField("player", uint1 / H3PlayerColors)
			* P(3)
			* If(
				P("\0\0\0\0"),
				CField("allowdFactions", CBitArray(9) / selectBitArray(H3FactionNames)),
				CField("factionSameAsTownId", hex(4))
			)
			* CField("level", MapObject_SubId / Inc)
	)

handle("RandomDwelling", MapObjectRandomDwelling)
handle("RandomDwellingFaction", MapObjectRandomDwellingFaction)
handle("RandomDwellingLevel", MapObjectRandomDwellingLevel)

handle("QuestGuard", MapFormatGE("AB") * MapObject_Quest)

local MapObjectHeroPlaceHolder = MapFormatGE("AB")
	* Ct(
		CField("owner", uint1 / H3PlayerColors)
			* If(Byte(0xff), CField("powerRating", uint1), CField("id", uint1 / H3Heroes))
	)

handle("HeroPlaceholder", MapObjectHeroPlaceHolder)
handle("CreatureBank", Ct(CField("type", MapObject_SubId / H3CreatureBankTypes)))

handle("BorderGate", MapFormatGE("AB") * Ct(CField("color", MapObject_SubId / H3ObjectColors)))

handle("Object", Ct(CField("wogId", MapObject_SubId)))

handle("ResourceWarehouse", MapFormatIs("HotA") * Ct(CField("sid", MapObject_SubId)))

handle("MagicalTerrain", MapFormatIs("HotA") * Ct(CField("sid", MapObject_SubId)))

handle("Building", MapFormatIs("HotA") * Ct(CField("sid", MapObject_SubId)))
handle("SeaObject", MapFormatIs("HotA") * Ct(CField("sid", MapObject_SubId)))
handle("Building2", MapFormatIs("HotA") * Ct(CField("sid", MapObject_SubId)))

local MapObject_AB = MapFormatGE("AB") * Ct(CField("sid", MapObject_SubId))

handle("FreelancersGuild", MapObject_AB)
handle("DirtHills", MapObject_AB)
handle("DesertHills", MapObject_AB)
handle("GrassHills", MapObject_AB)
handle("TradingPost2", MapObject_AB)
handle("Trees2", MapObject_AB)
handle("SwampFoliage", MapObject_AB)
handle("Lake2", MapObject_AB)
handle("RoughHills", MapObject_AB)
handle("SubterraneanRocks", MapObject_AB)

local MapObject_SoD = MapFormatGE("SoD") * Ct(CField("sid", MapObject_SubId))

handle("FavorableWinds", MapObject_SoD)
handle("CursedGround2", MapObject_SoD)
handle("MagicPlains2", MapObject_SoD)
handle("CloverField", MapObject_SoD)
handle("EvilFog", MapObject_SoD)
handle("FieryFields", MapObject_SoD)
handle("HolyGround", MapObject_SoD)
handle("LucidPools", MapObject_SoD)
handle("MagicClouds", MapObject_SoD)
handle("Rocklands", MapObject_SoD)

local MapObject_Wog = Ct(CField("sid", MapObject_SubId))

handle("WogUnknown", MapObject_Wog)

local MapObject = Ct(
	CField("pos", Position)
			* CField("tid", uint4)
			* P(5)
			* CField(
				"obj",
				Cmt(Cb("tid") * Cb("pos"), function(sub, pos, idx, position)
					local templates = globals.mapObjectTemplates
					assert(
						idx >= 0 and idx < #templates,
						string.format(
							"MapObject at 0x%x, tid=%d(0x%x) outof range(%d).",
							pos - 12 - 1,
							idx,
							idx,
							#templates
						)
					)
					globals.mapObjectTemplate = templates[idx + 1]
					local temp = templates[idx + 1]
					local oid = temp.id
					map_debug(
						'Loading MapObject at 0x%x,tid=%d,subid=%d,oid=%d,otype="%s",pos=[%d,%d,%d]\n',
						pos - 12 - 1,
						idx,
						temp.subId,
						oid,
						H3ObjectIds[oid],
						position.x,
						position.y,
						position.z
					)
					local obj_reader = MapObjectReader[oid]
					assert(
						obj_reader,
						string.format(
							"Dont know how to load object of type %d at 0x%x[%d,%d,%d]",
							oid,
							pos - 1,
							position.x,
							position.y,
							position.z
						)
					)
					local obj, npos
					obj, npos = (obj_reader * Cp()):match(sub, pos)
					assert(obj)
					obj.typeName = H3ObjectIds[oid]
					assert(obj.typeName)
					obj.sid = temp.subId
					return npos, obj
				end)
			)
		+ function()
			-- error handling, 其他信息上面已经dbg_print了，这里只补充template信息
			error(string.format('Load failed,template="%s"\n', rtk.tableToLuaCode(globals.mapObjectTemplate)))
		end
)
local MapObjects = CDynamicArray(uint4, MapObject, "mapObjects")

-- 可用英雄由两部分数据指定，一个bit array，一个byte array
local AllowedHeroesCount = MapFormatIs("HotA") * uint4 + MapFormatIs("RoE") * Cc(128) + Cc(160)
local ABMoreHeroes = Cmt(int4, function(sub, pos, cnt)
	map_debug("add %d heroes at 0x%x.\n", cnt, pos - 1)
	if cnt < 0 then
		return true, {}
	end
	local more_heroes = { sub:byte(pos, pos + cnt - 1) }
	return pos + cnt, more_heroes
end)
local function addMoreHeroes(arr, new_arr)
	local k, v
	map_debug("add hero new_arr=%s\n", rtk.tableToLuaCode(new_arr or { "INVALID" }))
	for k, v in ipairs(new_arr) do
		arr[v] = true
	end
	return arr
end
local AllowedHeroes = CDynamicBitArray(AllowedHeroesCount) * If(MapFormatGE("AB"), ABMoreHeroes % addMoreHeroes)
--[[
	* (CDynamicArray(MapFormatGE('AB') * uint4 + Cc(0),uint1) % function(arr,new_arr)
		local k,v
--		map_debug('arr=%s',tableToLuaCode(arr))
		map_debug('add hero new_arr=%s\n',tableToLuaCode(new_arr or {'INVALID'}))
		for k,v in ipairs(new_arr)do
			arr[v]=true
		end
		return arr
	end)
]]

local h3m =
	Ct(CField("info", MapInfo) * CField("players", CArray(8, Player, "Player") / function(players)
		local _, p
		for _, p in ipairs(players) do
			p.color = H3PlayerColors[_ - 1]
		end
		return players
	end) * CField("victoryCondition", VictoryCondition) * CField("lossCondition", LossCondition) * (NoTeam + TeamDef) * CField(
		"allHeroes",
		AllowedHeroes
			/ function(arr)
				local ret = {}
				ret.allowed = selectBitArray(H3Heroes)(arr)
				ret.disabled = selectBitArray(H3Heroes, false)(arr)
				ret.mask = arr
				return ret
			end
	) * If(MapFormatGE("SoD"), CField("heroCustomization", HeroCustomizationArray)) * P(31) * If(
		MapFormatIs("HotA"),
		CField("allowSpecialWeeks", bool)
	) * If(MapFormatGE("AB"), CField("allowedArtifacts", AllowedArtifacts)) * If(
		MapFormatGE("SoD"),
		CField("allowedSpells", AllowedSpells)
	) * If(MapFormatGE("SoD"), CField("allowedSecodarySkills", AllowedSecodarySkills)) * CField("rumors", Rumors) * If(
		MapFormatGE("SoD"),
		CField("heroConfigurationCustomizations", HeroConfigurationCustomizations)
	) * CField("terrain", Terrain) * If(function()
		return g_map_info.hasSecondLevel
	end, CField("underGround", Terrain)) * CField("mapObjectTemplates", MapObjectTemplates) * CGlobal(
		"mapObjectTemplates"
	) * CField("mapObjects", MapObjects) * CMarker("dbg_0") * CField(
		"events",
		CDynamicArray(uint4, Ct(TownEvent_Base))
	) * If(P(124), true, C(P(1) ^ 0) / function(s)
		rtk.printk(2, "Warning : 124 tail-bytes expected,got %d.\n", #s)
	end) * CMarker("eof"))

local function writefile(f, d)
	local fh = io.open(f, "wb")
	assert(fh, string.format("Cannot open %s for write\n", f))
	fh:write(d)
	fh:close()
	map_debug("Written %d bytes to %s\n", #d, f)
end

function h3MapReadFileRaw(f)
	local fh = io.open(f, "rb")
	assert(fh, string.format('Error openning "%s"', f))
	local str = fh:read("*a")
	fh:close()
	map_debug("Read %d bytes from %s\n", #str, f)
	if P("\x1f\x8b"):match(str) then
		local lz = require("zlib")
		local stream = lz.inflate()
		local dstr = stream(str)
		map_debug("Uncompressed %d -> %d bytes\n", #str, #dstr)
		str = dstr
	end
	return str
end

function h3MapGetInfo(file)
	local str = h3MapReadFileRaw(file)
	globals = { strings = {} }
	local info = MapInfo:match(str)
	return info
end

function h3MapReadData(data)
	globals = { strings = {} }
	local map = h3m:match(data)
	map.rawdata = data
	map.strings = globals.strings
	return map
end

function h3MapReadFile(file)
	local data = h3MapReadFileRaw(file)
	assert(data, string.format('Error reading "%s"\n', file))
	globals = { strings = {} }
	return h3MapReadData(data)
end

local uchardet
function h3MapDetectEncoding(Map)
	uchardet = uchardet or require("uchardet").create()
	local ud = uchardet
	local bytes = 0
	local _, ps
	local sample = {}
	for _, ps in ipairs(Map.strings) do
		if ps.len > 0 then
			table.insert(sample, ps.str)
			bytes = bytes + ps.len
			if bytes > 256 then
				break
			end
		end
	end
	ud:handle_data(table.concat(sample, ";"))
	ud:data_end()
	local ret = ud:result()
	if #ret == 0 then
		return
	end
	ret = ret[1]
	rtk.dbg_print("Detect encoding=%s,lang=%s\n", ret.encoding, ret.lang)
	return ret.encoding, ret.lang
end

function h3MapPatchStrings(map, strlist)
	local ibuffer = map.rawdata
	local ret = {}
	local i_ptr, o_ptr = 0, 0
	local isize = #ibuffer
	local _, pstr
	for _, pstr in ipairs(strlist) do
		local offset, size, str = pstr.pos, pstr.len, pstr.str
		assert(
			offset - 4 >= i_ptr,
			string.format("String %d, offset 0x%x goes under current i_ptr 0x%x.", _, offset, i_ptr)
		)
		assert(
			offset + size <= isize,
			string.format("String %d, offset+size=%x goes beyond file end(%x).", _, offset + size, isize)
		)
		if offset - 4 - i_ptr > 0 then
			rtk.dbg_print("Copy 0x%x bytes [0x%x,0x%x]\n", offset - 4 - i_ptr, i_ptr, offset - 4 - 1)
			table.insert(ret, ibuffer:sub(i_ptr + 1, offset - 4))
		end
		-- insert new size field(replacing old)
		table.insert(ret, gx.write_uint32(#str))
		o_ptr = o_ptr + offset - i_ptr
		-- insert translated string
		table.insert(ret, str)
		-- update src pointer(skip original string)
		i_ptr = offset + size
		rtk.dbg_print('Patched [0x%x,0x%x]->[0x%x,0x%x],str="%s"\n', offset, i_ptr, o_ptr, o_ptr + #str - 1, str)
		o_ptr = o_ptr + #str
	end
	if isize > i_ptr then
		table.insert(ret, ibuffer:sub(i_ptr + 1, isize))
		rtk.dbg_print("Copied 0x%x bytes [0x%x,0x%x]\n", isize - i_ptr, i_ptr, isize - 1)
	end
	ret = table.concat(ret)
	rtk.dbg_print("Ok, %d strings patched, file %d->%d bytes.\n", #strlist, #ibuffer, #ret)
	return ret
end
